import { OpenAPIV3 } from 'openapi-types';
import Ajv, { ValidateFunction } from 'ajv';
import { ApiError } from '@/request';
import { ApiResult } from '@/request/core/ApiResult';
import { ApiRequestOptions } from '@/request/core/ApiRequestOptions';
import { OdinException } from '@/exception';
import { GuiErrCode } from '@/exception/error-code';
import { log } from '@/tauri-plugins/log';
import createDebug from '@/contrib/debug';

const debug = createDebug('worker:schema-check');

type openApiSchema = {
  schema: OpenAPIV3.SchemaObject;
}

type schemaType = {
  $schema: string;
  type: string;
  properties: Record<string|number, object>,
  definitions: object,
  components: object,
}

function recursiveTransformOpenAPIV3Definitions(object: any) {
  // Transformations //
  // OpenAPIV3 nullable
  if (object.nullable === true) {
    if (object.enum) {
      // Enums can not be null with type null
      object.oneOf = [
        { type: 'null' },
        {
          type: object.type,
          enum: object.enum,
        },
      ];
      delete object.type;
      delete object.enum;
    } else if (object.type) {
      object.type = [object.type, 'null'];
    } else if (object.allOf) {
      object.anyOf = [{ allOf: object.allOf }, { type: 'null' }];
      delete object.allOf;
    } else if (object.oneOf || object.anyOf) {
      const arr: any[] = object.oneOf || object.anyOf;
      arr.push({ type: 'null' });
    }

    delete object.nullable;
  }
  // Remove writeOnly properties from required array
  if (object.properties && object.required) {
    const writeOnlyProps = Object.keys(object.properties).filter(
      (key) => object.properties[key].writeOnly,
    );
    writeOnlyProps.forEach((value) => {
      const index = object.required.indexOf(value);
      object.required.splice(index, 1);
    });
  }

  Object.keys(object).forEach((attr) => {
    if (typeof object[attr] === 'object' && object[attr] !== null) {
      recursiveTransformOpenAPIV3Definitions(object[attr]);
    } else if (Array.isArray(object[attr])) {
      object[attr].forEach((obj: any) => recursiveTransformOpenAPIV3Definitions(obj));
    }
  });
}

function transformOpenAPIV3Definitions(schema: schemaType) {
  if (typeof schema !== 'object') {
    return schema;
  }

  // deep copy 不需要处理类型安全
  const res = JSON.parse(JSON.stringify(schema));
  recursiveTransformOpenAPIV3Definitions(res);
  return res;
}

// function compileValidators(v: Ajv, schemas: Record<string, schemaType>) {
//   const validators: Record<string, ValidateFunction> = {};

//   Object.keys(schemas).forEach((name) => {
//     const val = schemas[name];
//     validators[name] = v.compile(transformOpenAPIV3Definitions(val));
//   });

//   return validators;
// }

function getSchemas(responses: Record<string, openApiSchema>, components: OpenAPIV3.ComponentsObject): Record<string, schemaType> {
  const schemas:Record<string, schemaType> = {};

  Object.keys(responses).forEach((name) => {
    const response = responses[name];
    const { schema } = response;
    schemas[name] = {
      $schema: 'http://json-schema.org/schema#',
      type: 'object',
      properties: {
        response: schema,
      },
      components: components || {},
      definitions: components || {},
    };
  });

  return schemas;
}

function IsReference(obj: any): boolean {
  if (!!obj && obj.$ref !== null
    && obj.$ref !== undefined
    && obj.$ref.length !== null
    && obj.$ref.length !== undefined
    && typeof (obj.$ref) === 'string') {
    return true;
  }
  return false;
}

/*
ref: #/components/responses/NotFoundResponse
*/
function parseResponse(doc: OpenAPIV3.Document, ref: string): OpenAPIV3.ResponseObject|null {
  const prefix = '#/components/responses/';
  if (!ref.startsWith(prefix)) {
    console.warn(`[${ref}] reference not start with [${prefix}]`);
    return null;
  }
  const key = ref.slice(prefix.length);
  if (!doc.components || !doc.components.responses) {
    console.warn(`[${ref}] components or response empty`);
    return null;
  }
  const { responses } = doc.components;
  if (!responses[key]) {
    console.warn(`[${ref}] cannot found response [${key}]`);
    return null;
  }
  const r0 = responses[key];
  if (!IsReference(r0)) {
    return r0 as OpenAPIV3.ResponseObject;
  }
  // ref parse
  const r1 = r0 as OpenAPIV3.ReferenceObject;
  if (r1.$ref.startsWith(prefix)) {
    return parseResponse(doc, r1.$ref);
  }

  console.warn(`[${ref}] parse failed.`);
  return null;
}

type methodLowerCaseType = Lowercase<ApiRequestOptions['method']>

export function checkJsonSchema(req: ApiRequestOptions, doc: OpenAPIV3.Document, result: ApiResult): boolean {
  const code = result.status;
  const obj = result.body;
  let operation: OpenAPIV3.OperationObject|null = null;
  // eslint-disable-next-line
  for (const path in doc.paths) {
    const pathObj = doc.paths[path];
    if (!pathObj) {
      console.warn(`${path} not exists`);
      continue;
    }
    if (req.url !== path) {
      continue;
    }
    const oprObj = pathObj[req.method.toLowerCase() as methodLowerCaseType];
    if (!oprObj) {
      continue;
    }
    operation = oprObj;
  }
  if (operation === null) {
    console.warn(`[${req.method} => ${req.url}] cannot found operationId`);
    return false;
  }
  const { responses } = operation;
  if (!responses) {
    console.warn(`[${req.method} => ${req.url}] responses empty!`);
    return false;
  }
  if (!responses[code]) {
    console.warn(`[${req.method} => ${req.url}] not found response code [${code}]`);
    return false;
  }
  const respRef = responses[code];
  let resp: OpenAPIV3.ResponseObject|null = null;
  if (IsReference(respRef)) {
    resp = parseResponse(doc, (respRef as OpenAPIV3.ReferenceObject).$ref);
  } else {
    resp = respRef as OpenAPIV3.ResponseObject;
  }
  if (!resp) {
    console.warn(`[${req.method} => ${req.url}] [${code}] response or reference parse failed`);
    return false;
  }
  if (!resp.content) {
    console.warn(`[${req.method} => ${req.url}] [${code}] response content declare empty`);
    return false;
  }
  const respContent = resp.content;
  const respJson = respContent['application/json'];
  if (!respJson) {
    console.warn(`[${req.method} => ${req.url}] [${code}] schema not declared valid json response`);
    return false;
  }
  const jsonSchema = respJson.schema;
  if (!jsonSchema) {
    console.warn(`[${req.method} => ${req.url}] [${code}] json content empty`);
    return false;
  }
  let rsSchema: any;
  if (IsReference(jsonSchema)) {
    rsSchema = {
      $ref: (jsonSchema as OpenAPIV3.ReferenceObject).$ref,
    };
  } else {
    rsSchema = jsonSchema as OpenAPIV3.SchemaObject;
  }
  const rs: Record<string, openApiSchema> = {};
  rs[code] = {
    schema: rsSchema,
  };

  const components = doc.components ? doc.components : {};
  const schemas = getSchemas(rs, components);
  const v = new Ajv({
    useDefaults: true,
    allErrors: true,
    strict: false,
    logger: false,
  });

  const validator: ValidateFunction = v.compile(transformOpenAPIV3Definitions(schemas[code]));
  const isValid = validator({
    response: obj === undefined ? null : obj,
  });

  if (!isValid) {
    const errs = validator.errors || [];
    try {
      const errListStr = JSON.stringify(errs);
      const bodyStr = JSON.stringify(result.body);
      log(`json schema failed [${errListStr}], [${bodyStr}]`);
    } catch (e) {
      console.warn('output json schema check failed');
      console.warn(e);
    }
    debug(errs);
    debug(result.body);
    console.log('接口校验失败1', errs);
    console.log('接口校验失败2', result.body);
    const origin = new ApiError(req, result, 'json schema check failed');
    // 错误是 来自服务端 错误码由客户端定义 json 检查失败
    throw new OdinException('json check failed').withOrigin(origin).withCategory('API-SERVER').withCode(GuiErrCode.JsonCheckFailed);
  }

  return true;
}
